{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## Открытый курс по машинному обучению. Сессия № 2\n",
    "\n",
    "### <center> Автор материала: Андрей Сухарев (@fremis)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## <center> Индивидуальный проект по анализу данных </center>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**План исследования**\n",
    " - Описание набора данных и признаков\n",
    " - Первичный анализ признаков\n",
    " - Первичный визуальный анализ признаков\n",
    " - Закономерности, \"инсайты\", особенности данных\n",
    " - Предобработка данных\n",
    " - Создание новых признаков и описание этого процесса\n",
    " - Кросс-валидация, подбор параметров\n",
    " - Построение кривых валидации и обучения \n",
    " - Прогноз для тестовой или отложенной выборки\n",
    " - Оценка модели с описанием выбранной метрики\n",
    " - Выводы\n",
    " \n",
    " Более детальное описание [тут](https://goo.gl/cJbw7V)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "from matplotlib import pyplot as plt\n",
    "%matplotlib inline\n",
    "\n",
    "import librosa\n",
    "import librosa.display\n",
    "import glob\n",
    "import random\n",
    "\n",
    "np.random.seed(42)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  1. Описание набора данных и признаков"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Почти каждый день появляются новости, в которых разные виды нейронных сетей ставят очередную планку качества в решении той или иной задачи. В частности, в последние годы большой прирост качества мы можем наблюдать в решении задачи классификации изображений при помощи искусственных нейронных сетей, а именно свёрточных нейронных сетей (CNN).\n",
    "\n",
    "Может сложиться впечатление, что нейронные сети могут решить абсолютно любую задачу (и ведь это отчасти так: нейронная сеть является универсальным аппроксиматором при достаточном количестве слоёв), и другие модели машинного обучения вроде логистической регрессии и градиентного бустинга на решающих деревьях больше не нужны.\n",
    "\n",
    "Однако, если вы посмотрите, что общего между задачами, решаемыми при помощи нейронных сетей, то заметите, что входными данными для них выступают:\n",
    "* Изображения (классификация, стилизация и т.п.)\n",
    "* Звуковые сигналы (распознавание речи и т.п.)\n",
    "* Текст (обработка естесственного языка)\n",
    "\n",
    "Все эти типы данных объединяет одно: <i>локальные свойства</i>. Если в типичной задаче, решаемой xgboost'ом, вам на вход подаётся таблица признаков, порядок которых абсолютно неважен (какая разница, выставите вы сначала \"пол\", потом \"возраст\" или наоборот), то в изображениях группы соседних пикселей описывают какой-нибудь признак (например, нос котика). То же самое касается звуковых сигналов: последовательность частот в соседних звуковых интервалах может иметь свой смысл, который можно использовать для решения задачи.\n",
    "\n",
    "В данном проекте будет рассмотрена задача классификации звуковых сигналов при помощи свёрточной нейронной сети.\n",
    "\n",
    "Данная задача имеет множество применений, как то:\n",
    "* Идентификация говорящего\n",
    "* Определение интонации и настроения говорящего (воскрицательная, вопросительная интонации, гнев, радость и т.д.)\n",
    "* Определение животного перед микрофоном в лесу\n",
    "* и др.\n",
    "\n",
    "Но больше интересно не её самостоятельное применение, а в совокупности с другими.\n",
    "\n",
    "Решаться задача будет на общедоступном наборе данных <a href=\"https://github.com/karoldvl/ESC-50\">ESC-50</a>, созданном польским исследователем Karol J. Piczak на основе звуков с Freesound. \n",
    "\n",
    "Соответственно, на входе мы имеем звуковой сигнал с заданной частотой дискретизации. Необходимо определить, к какому классу он относится.\n",
    "\n",
    "Всю необходимую информацию о файле можно найти в его названии и пути:\n",
    "```\n",
    "category_id - category_name/fold_number-Freesound_clip_ID-take_letter.ogg\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  2. Первичный анализ данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В данном наборе находится 2000 звуковых файлов, размеченных по 50 классам: лай собаки, плач ребёнка, стук в дверь и т.д. Полное описание всех классов можно найти на странице с набором данных. Набор уже разделён на 5 фолдов. Набор является сбалансированным, т.е. на каждый класс в каждом фолде приходится одинаковое число записей."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Частота дискретизации файлов: 44 кГц, содержатся они в контейнере ogg."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на примеры данных."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "all_recordings = glob.glob('ESC-50/*/*.ogg')\n",
    "\n",
    "def plot_wave(audio_name):\n",
    "    x, sr = librosa.load(audio_name)\n",
    "\n",
    "    plt.figure(figsize=(12, 4))\n",
    "    plt.title(audio_name)\n",
    "    librosa.display.waveplot(x, sr=sr);\n",
    "\n",
    "plot_wave(all_recordings[42])\n",
    "plot_wave(all_recordings[300])\n",
    "plot_wave(all_recordings[1488])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Можно увидеть, что звуковые сигналы не всегда имеют настоящую продолжительность в 5 секунд. При разрезании на кусочки данный момент будет учитываться, что описано в разделе с предобработкой."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "Также можно увидеть, что амплитуда колебаний по модулю не превосходит 1, значит дополнительная нормализация не требуется."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3 и 4. Первичный визуальный анализ данных. Инсайты, найденные зависимости"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Визуализируем логарифмированные мел-спектрограмммы звуковых сигналов, которые будут служить основой для нашей модели"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bands = 60\n",
    "\n",
    "FFT_WINDOW = 1024\n",
    "HOP = 512\n",
    "\n",
    "def plot_spec(file_name):\n",
    "    X, sr = librosa.load(file_name)\n",
    "\n",
    "    S = librosa.feature.melspectrogram(X,\n",
    "                                       n_fft=FFT_WINDOW, hop_length=HOP,\n",
    "                                       sr=22050, n_mels=bands)\n",
    "    logS = librosa.power_to_db(S, ref=np.max)\n",
    "    plt.figure(figsize=(15, 7))\n",
    "    plt.title(file_name)\n",
    "    librosa.display.specshow(logS,\n",
    "                             y_axis='mel',\n",
    "                             x_axis='time')\n",
    "    plt.colorbar(format='%+2.0f dB')\n",
    "    plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на примеры лая собак."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dog1_name = os.path.join('ESC-50', '101 - Dog', '1-30226-A.ogg')\n",
    "dog2_name = os.path.join('ESC-50', '101 - Dog', '1-30344-A.ogg')\n",
    "dog3_name = os.path.join('ESC-50', '101 - Dog', '1-32318-A.ogg')\n",
    "\n",
    "plot_spec(dog1_name)\n",
    "plot_spec(dog2_name)\n",
    "plot_spec(dog3_name)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Можно увидеть, что лай собак имеет чёткие всплески короткой продолжительности практически во всём спектре. Раз это можно заметить глазом, значит и сетка это вполне способна увидеть."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "laugh1_name = os.path.join('ESC-50', '307 - Laughing', '1-1791-A.ogg')\n",
    "laugh2_name = os.path.join('ESC-50', '307 - Laughing', '1-30039-A.ogg')\n",
    "laugh3_name = os.path.join('ESC-50', '307 - Laughing', '1-30043-A.ogg')\n",
    "\n",
    "plot_spec(laugh1_name)\n",
    "plot_spec(laugh2_name)\n",
    "plot_spec(laugh3_name)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Как можно увидеть, даже визуальный анализ позволяет нам определить, что за класс у данной записи. У данных видна чёткая локальная структура, а следовательно, почему бы не попытаться использовать методы, которые применяются для анализа изображений?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Более подробный визуальный анализ набора данных ESC-50 вы можете найти в <a href=\"http://nbviewer.jupyter.org/github/karoldvl/paper-2015-esc-dataset/blob/master/Notebook/ESC-Dataset-for-Environmental-Sound-Classification.ipynb\">тетрадке автора</a>. Мог бы продублировать его работу, но не буду."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Выбор метрики"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Набор данных ESC-50 идеально сбалансирован, т.е. для каждого класса существует одинаковое количество файлов длиной до 5 секунд, и они поровну разбиты на 5 фолдов. Однако, после того, как эти файлы будут разбиты на фрагменты, количество станет неравным из-за того, что в каких-то классах одержится больше тишины, а в каких-то меньше, но всё равно будет примерно одинаковым.\n",
    "\n",
    "Поэтому для оценки качества модели будут использоваться 2 метрики: accuracy (доля правильных ответов), т.к. она адекватно оценивает качество на сбалансированных выборках, а также F1-score, которая позволяет сгладить неравенство классов и взвешенно оценить работу на основе точности и полноты (а также weighted вариант позволяет придать разные веса классам в зависимости от их численности). Будут приведены метрики как для отдельных фрагментов, так и для файлов целиком.\n",
    "\n",
    "AUC-ROC не получается применить на данном наборе ESC-50, т.к. из-за небольшого количества примеров (целых звуковых файлов) в одном фолде (8 штук) часто получается, что у пары классов и точность, и полнота составляет 0, а AUC-ROC в реализации scikit-learn не умеет работать с такими значениями. Возможно, это какой-то баг, потому что в этом случае выдаётся предупреждение, что количество классов меньше, чем количество лейблов, а в некоторых случаях всё проходит нормально, и при обработке других наборов данных тем же кодом всё работает нормально."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Выбор модели "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Рассматриваемый набор данных находится на грани того, чтобы назвать его маленьким для глубоких сетей. Например, набора ESC-10 уже будет недостаточно, модель быстро переобучится на небольшом наборе данных (его можно искусственно увеличить путём сжатий/растяжений, увеличения/уменьшения тональности и других приёмов).\n",
    "\n",
    "Поэтому для данной задачи соберём относительно неглубокую сеть, больше всего похожую на <a href=\"https://en.wikipedia.org/wiki/AlexNet\">AlexNet</a>. Более глубокие сети не дают значимого улучшения производительности модели на данном наборе, зато сильно замедляют обучение и требуют более упорной борьбы с переобучением."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "from collections import Counter\n",
    "import numpy as np\n",
    "from keras.callbacks import ModelCheckpoint, ReduceLROnPlateau, EarlyStopping\n",
    "from keras.models import load_model\n",
    "from keras.utils import to_categorical\n",
    "from sklearn.metrics import f1_score, accuracy_score, classification_report\n",
    "                            \n",
    "import shelve"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Переопределим масштабирование признаков, т.к. стандартный пакет из scikit-learn не умеет в многомерные данные."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "EPS = 1e-6\n",
    "\n",
    "class StandardScaler:\n",
    "    def __init__(self):\n",
    "        # For multichannel\n",
    "        self.means = []\n",
    "        self.stds = []\n",
    "        # For single channel\n",
    "        self.mean = None\n",
    "        self.std = None\n",
    "\n",
    "    def fit(self, X):\n",
    "        if len(X.shape) == 4:\n",
    "            channels = X.shape[-1]\n",
    "            for i in range(channels):\n",
    "                self.means.append(X[:, :, :, i].mean())\n",
    "                self.stds.append(X[:, :, :, i].std())\n",
    "        else:\n",
    "            self.mean = X.mean()\n",
    "            self.std = X.std()\n",
    "\n",
    "    def transform(self, X, copy=True):\n",
    "        if copy:\n",
    "            res = np.copy(X)\n",
    "        else:\n",
    "            res = X\n",
    "\n",
    "        if len(X.shape) == 4:\n",
    "            channels = X.shape[-1]\n",
    "            for i in range(channels):\n",
    "                res[:, :, :, i] = (X[:, :, :, i]-self.means[i])/(self.stds[i]+EPS)\n",
    "        else:\n",
    "            res = (X-self.mean)/(self.std+EPS)\n",
    "\n",
    "        return res\n",
    "\n",
    "    def fit_transform(self, X, copy=True):\n",
    "        self.fit(X)\n",
    "        return self.transform(X, copy)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Базовая логика обучения модели"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class BaseModel:\n",
    "    def __init__(self, name='model', verbose=1):\n",
    "        self.model = None\n",
    "        self.history = None\n",
    "        self.name = name\n",
    "        self.models_path = 'trained_models'\n",
    "        self.scaler = StandardScaler()\n",
    "        self.verbose = verbose\n",
    "\n",
    "    def get_keras_model(self, input_shape, classes_count):\n",
    "        raise NotImplementedError('model is not specified')\n",
    "\n",
    "    def get_model_save_dir(self, dataset):\n",
    "        path = os.sep.join((self.models_path, self.name,\n",
    "                            dataset.name, dataset.extraction_type))\n",
    "        if not os.path.exists(path):\n",
    "            os.makedirs(path)\n",
    "        return path\n",
    "\n",
    "    def get_model_path(self, dataset, test_folds):\n",
    "        save_dir = self.get_model_save_dir(dataset)\n",
    "        folds_suff = '_'.join(test_folds)\n",
    "        model_path = save_dir+os.sep+folds_suff+'.h5'\n",
    "        return model_path\n",
    "\n",
    "    def get_meta_path(self, dataset, test_folds):\n",
    "        save_dir = self.get_model_save_dir(dataset)\n",
    "        folds_suff = '_'.join(test_folds)\n",
    "        meta_path = save_dir+os.sep+folds_suff+'.shelve'\n",
    "        return meta_path\n",
    "\n",
    "    def try_load(self, dataset, test_folds, input_shape=None, classes_count=None):\n",
    "        model_path = self.get_model_path(dataset, test_folds)\n",
    "        meta_path = self.get_meta_path(dataset, test_folds)\n",
    "        if os.path.exists(model_path):\n",
    "            if input_shape is None or classes_count is None:\n",
    "                self.model = load_model(model_path)\n",
    "            else:\n",
    "                # This is due to bug in Keras with loading functional models\n",
    "                self.model = self.get_keras_model(input_shape, classes_count)\n",
    "                self.model.load_weights(model_path)\n",
    "            with shelve.open(meta_path) as meta:\n",
    "                if self.verbose:\n",
    "                    print('Loading meta info')\n",
    "                self.history = meta['history']\n",
    "                self.scaler = meta['scaler']\n",
    "            return True\n",
    "        return False\n",
    "\n",
    "    def _predict_by_features(self, X, voting='prob'):\n",
    "        if voting == 'prob':\n",
    "            probs = self.model.predict(X, verbose=0)\n",
    "            return np.sum(probs, axis=0).argmax()\n",
    "        elif voting == 'major':\n",
    "            classes = self.model.predict_classes(X, verbose=0)\n",
    "            c = Counter(classes)\n",
    "            return c.most_common(1)[0][0]\n",
    "        else:\n",
    "            raise NotImplementedError('unknown voting:', voting)\n",
    "\n",
    "    def _predict_proba(self, X):\n",
    "        probs = self.model.predict(X, verbose=0)\n",
    "        res = np.sum(probs, axis=0)\n",
    "        res /= np.sum(res)\n",
    "        return res\n",
    "\n",
    "    def _validate_on_fold(self, X_fold, y_fold,\n",
    "                          dataset, fold_name, voting='prob'):\n",
    "        fold_idx = dataset.fold_feat_idx[fold_name]\n",
    "\n",
    "        y_true = []\n",
    "        y_pred = []\n",
    "        y_pred_proba = []\n",
    "\n",
    "        for idx_from, idx_to in fold_idx:\n",
    "            X = X_fold[idx_from:idx_to]\n",
    "            y = y_fold[idx_from]\n",
    "            y_pred_s = self._predict_by_features(X, voting)\n",
    "            y_pred_pr = self._predict_proba(X)\n",
    "            y_true.append(y)\n",
    "            y_pred.append(y_pred_s)\n",
    "            y_pred_proba.append(y_pred_pr)\n",
    "\n",
    "        y_true_oh = to_categorical(y_true, num_classes=dataset.metadata.classes_count)\n",
    "        results = TestResults(y_true, y_pred, y_true_oh, y_pred_proba,\n",
    "                              classes=dataset.metadata.classes)\n",
    "        return results\n",
    "\n",
    "    def train_on(self, dataset, train_folds, test_folds, epochs=10,\n",
    "                 batch_size=256, validate=True, min_lr=0.0002, ignore_trained=False):\n",
    "        X_train, X_test, y_train, y_test = dataset.train_test_split(train_folds, test_folds)\n",
    "        X_train = self.scaler.fit_transform(X_train, copy=False)\n",
    "        X_test = self.scaler.transform(X_test, copy=False)\n",
    "        y_train_oh = to_categorical(y_train, num_classes=dataset.metadata.classes_count)\n",
    "        y_test_oh = to_categorical(y_test, num_classes=dataset.metadata.classes_count)\n",
    "\n",
    "        if self.verbose:\n",
    "            print('Train set statistics:')\n",
    "            unique, counts = np.unique(y_train, return_counts=True)\n",
    "            for i in range(len(unique)):\n",
    "                print('{0} - {1} segments'.format(unique[i], counts[i]))\n",
    "            print('Test set statistics:')\n",
    "            unique, counts = np.unique(y_test, return_counts=True)\n",
    "            for i in range(len(unique)):\n",
    "                print('{0} - {1} segments'.format(unique[i], counts[i]))\n",
    "\n",
    "        input_shape = X_train.shape[1:]\n",
    "        classes_count = dataset.metadata.classes_count\n",
    "\n",
    "        if not ignore_trained and self.try_load(dataset, test_folds, input_shape, classes_count):\n",
    "            if self.verbose:\n",
    "                print('Found already trained model for this dataset and folds')\n",
    "        else:\n",
    "            self.model = self.get_keras_model(input_shape, classes_count)\n",
    "            model_path = self.get_model_path(dataset, test_folds)\n",
    "            best_saver = ModelCheckpoint(model_path,\n",
    "                                         monitor='val_acc',\n",
    "                                         verbose=1, save_best_only=True)\n",
    "            reduce_lr = ReduceLROnPlateau(monitor='val_loss', factor=0.75,\n",
    "                                          patience=7, min_lr=min_lr, verbose=1)\n",
    "            early_stop = EarlyStopping(monitor='val_loss', min_delta=0.0001,\n",
    "                                       patience=25, verbose=1)\n",
    "            callbacks = [best_saver, reduce_lr, early_stop]\n",
    "            history = self.model.fit(X_train, y_train_oh,\n",
    "                                     validation_data=(X_test, y_test_oh),\n",
    "                                     epochs=epochs,\n",
    "                                     batch_size=batch_size,\n",
    "                                     callbacks=callbacks)\n",
    "            self.history = history.history\n",
    "            meta_path = self.get_meta_path(dataset, test_folds)\n",
    "            # Saving all meta information about this training\n",
    "            with shelve.open(meta_path) as meta:\n",
    "                meta['history'] = self.history\n",
    "                meta['scaler'] = self.scaler\n",
    "\n",
    "        if validate:\n",
    "            if self.verbose:\n",
    "                print('Reloading best model to validate')\n",
    "            self.try_load(dataset, test_folds, input_shape, classes_count)\n",
    "\n",
    "            results = self._validate_on_fold(X_test, y_test, dataset,\n",
    "                                             fold_name=test_folds[0], voting='prob')\n",
    "            frag_acc = self.model.evaluate(X_test, y_test_oh, batch_size=128, verbose=0)[1]\n",
    "            results.frag_acc = frag_acc\n",
    "            return results\n",
    "\n",
    "    def predict_file(self, file_name, dataset, return_probs=False):\n",
    "        X, sample_rate, _ = dataset.feature_extractor.extract_features(file_name)\n",
    "        X = self.scaler.transform(X)\n",
    "        if return_probs:\n",
    "            return self._predict_proba(X)\n",
    "        else:\n",
    "            return self._predict_by_features(X)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Определение самой AlexNet-подобной сети. В качестве регуляризации используется <a href=\"https://en.wikipedia.org/wiki/Dropout_(neural_networks)\">Dropout</a>. Обучение производится при помощи модификации стохастического градиентного спуска — <a href=\"https://arxiv.org/abs/1412.6980\">алгоритма Адама</a>. Вместо полносвязных слоёв в конце используется <a href=\"https://arxiv.org/pdf/1312.4400.pdf\">Global average pooling</a>. Модель реализована при помощи библиотеки Keras, в качестве бэкенда к которой используется Tensorflow."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.models import Sequential\n",
    "from keras.layers import Dense, Conv2D, MaxPooling2D, Dropout, Activation, Input\n",
    "from keras.layers.normalization import BatchNormalization\n",
    "from keras.regularizers import l2\n",
    "from keras.layers.pooling import GlobalAveragePooling2D\n",
    "from keras.optimizers import SGD, Adam, Adadelta\n",
    "\n",
    "\n",
    "def add_conv_relu_pool_block(model, filters, kernel_size, pool_size=(2, 2), padding='same',\n",
    "                             batch_norm=True, dropout=None, input_shape=None):\n",
    "    if input_shape is not None:\n",
    "        model.add(Conv2D(filters, kernel_size=kernel_size,\n",
    "                         padding=padding, kernel_regularizer=l2(0.001),\n",
    "                         input_shape=input_shape))\n",
    "    else:\n",
    "        model.add(Conv2D(filters, kernel_size=kernel_size,\n",
    "                         padding=padding, kernel_regularizer=l2(0.001)))\n",
    "    model.add(Activation('relu'))\n",
    "    model.add(MaxPooling2D(pool_size=pool_size, padding=padding))\n",
    "\n",
    "    if batch_norm:\n",
    "        model.add(BatchNormalization())\n",
    "    if dropout:\n",
    "        model.add(Dropout(dropout))\n",
    "\n",
    "\n",
    "class AlexLikeModel(BaseModel):\n",
    "    def __init__(self, lr=0.001, suff=''):\n",
    "        super().__init__(name='alex_like'+suff)\n",
    "        self.lr = lr\n",
    "        self.suff = suff\n",
    "\n",
    "    def get_keras_model(self, input_shape, classes_count):\n",
    "        model = Sequential()\n",
    "\n",
    "        add_conv_relu_pool_block(model, 32, (7, 7), input_shape=input_shape, dropout=0.3)\n",
    "        add_conv_relu_pool_block(model, 64, (5, 5), dropout=0.3)\n",
    "        add_conv_relu_pool_block(model, 128, (3, 3), dropout=0.3)\n",
    "        add_conv_relu_pool_block(model, 256, (3, 3), dropout=0.3)\n",
    "\n",
    "        model.add(GlobalAveragePooling2D())\n",
    "        model.add(Dense(classes_count, activation='softmax'))\n",
    "\n",
    "        adam = Adam(lr=self.lr, decay=2e-5)\n",
    "        model.compile(loss='categorical_hinge',\n",
    "                      optimizer=adam,\n",
    "                      metrics=['accuracy'])\n",
    "        return model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. Предобработка данных"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Хотя исходные звуковые файлы имеют более-менее фиксированную продолжительность в 5 секунд, в произвольном наборе данных это может быть не так.\n",
    "Поэтому с целью приведения признаков в единой размерности, а также для увеличения набора данных, разобьём каждый исходный звуковой сигнал на несколько маленьких. При этом соседние фрагменты будут пересекаться между собой. В файлах будут встречаться фрагменты, которые содержат преимущественно тишину (это можно считать за пропуски), поэтому такие фрагменты не добавляются в выборки. Если их не удалять, то во время обучения сеть натыкается на них и сходит с ума: сеть начинает предсказывать что угодно как один единственный класс.\n",
    "\n",
    "<b>Важно</b>: пересечённые фрагменты не попадают в разные выборки, т.к. иначе будет происходить дикое переобучение (сталкивался с этим в разработке этого решения).\n",
    "\n",
    "Предсказание класса производится для каждого отдельного фрагмента, после чего вероятности принадлежности классам каждого фрагмента из файла складываются и нормируются, что и является итоговым предсказанием."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import librosa\n",
    "\n",
    "class SoundFeatureExtractor:\n",
    "    def __init__(self, metadata, verbose=1):\n",
    "        self.metadata = metadata\n",
    "        self.extend = False\n",
    "        self.stretch_rates = None\n",
    "        self.sample_rate = 22050\n",
    "        self.segment_overlap = 2\n",
    "        self.verbose = verbose\n",
    "\n",
    "    def sound_segments(self, sound_clip, window_size,\n",
    "                       stretch=True):\n",
    "        start = 0\n",
    "        while start < len(sound_clip):\n",
    "            segment = sound_clip[start:start+window_size]\n",
    "            if len(segment) == window_size:\n",
    "                yield segment\n",
    "            elif self.extend and len(segment) > window_size//2:\n",
    "                ext_segment = librosa.util.fix_length(segment, window_size)\n",
    "                yield ext_segment\n",
    "            start += window_size//self.segment_overlap\n",
    "\n",
    "    def aug_sound_segments(self, sound_clip, window_size,\n",
    "                           stretch=True):\n",
    "        clips = []\n",
    "\n",
    "        if stretch and self.stretch_rates is not None:\n",
    "            for rate in self.stretch_rates:\n",
    "                clips.append(librosa.effects.time_stretch(sound_clip, rate))\n",
    "\n",
    "        for clip in clips:\n",
    "            yield from self.sound_segments(clip, window_size, stretch)\n",
    "\n",
    "    def read_from_file(self, file_path):\n",
    "        if self.verbose:\n",
    "            print('Reading file:', file_path)\n",
    "        X, sample_rate = librosa.load(file_path, sr=self.sample_rate)\n",
    "        if self.verbose:\n",
    "            print('Extracted sound info of duration:', librosa.get_duration(X, sr=sample_rate))\n",
    "        return X, sample_rate\n",
    "\n",
    "    def extract_wave(self, file_name):\n",
    "        X, sample_rate = self.read_from_file(file_name)\n",
    "        return X, sample_rate"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Поскольку исходный звуковой сигнал представляет собой зависимость амплитуды от времени, а этой информации недостаточно, то воспользуемся стандартной техникой получения частотно-временных характеристик — преобразованием Фурье, а конкретнее оконным преобразованием Фурье. Затем полученный результат приведём к мел-шкале, а потом логарифмируем.\n",
    "\n",
    "Таким образом, мы получим стандартное представление звукового сигнала для обработки моделями машинного обучения — логарифмированную мелспектрограмму. Ещё одним похожим представлением является MFCC (мел-частотные кепстральные коэффициенты), которое показывает примерно то же самое качество в нейросетевых моделях, и поэтому оно не рассматривается в данном проекте.\n",
    "\n",
    "Также записи приводятся к более низкой частоте дискретизации 16 кГц, что позволяет убрать избыточные данные и лучше бороться с переобучением.\n",
    "\n",
    "Итоговые признаки: логарифмированная мел-спектрограмма с дельта-коэффициентами, частота дискретизации 16 кГц, длина окна Фурье: 30 мс с пересечением пополам."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "BANDS = 60\n",
    "FRAMES = 41\n",
    "FFT_WINDOW = 2048\n",
    "HOP_LENGTH = 512\n",
    "\n",
    "class LogMelSpecExtractor(SoundFeatureExtractor):\n",
    "    def __init__(self, metadata, bands=BANDS, frames=FRAMES,\n",
    "                 fft_window=FFT_WINDOW, hop_length=HOP_LENGTH,\n",
    "                 sample_rate=22050, extend=False, stretch_rates=None,\n",
    "                 verbose=1):\n",
    "        super().__init__(metadata, verbose)\n",
    "        self.bands = bands\n",
    "        self.frames = frames\n",
    "        self.fft_window = fft_window\n",
    "        self.hop_length = hop_length\n",
    "        self.window_size = fft_window*(frames-1)//self.segment_overlap\n",
    "        self.extend = extend\n",
    "        self.sample_rate = sample_rate\n",
    "        self.stretch_rates = stretch_rates\n",
    "\n",
    "    def _should_skip_silent_segment(self, logspec):\n",
    "        std = np.std(logspec)\n",
    "        if std < 0.2:\n",
    "            if self.verbose:\n",
    "                print('Skipping segment as it is almost silence. Mean: %.6f, Std: %.6f'%(logspec.mean(), std))\n",
    "            return True\n",
    "        return False\n",
    "\n",
    "    def _extract_feature(self, sound_segment):\n",
    "        melspec = librosa.feature.melspectrogram(sound_segment, n_fft=self.fft_window,\n",
    "                                                 hop_length=self.hop_length,\n",
    "                                                 sr=self.sample_rate, n_mels=self.bands)\n",
    "        logspec = librosa.logamplitude(melspec, ref=np.max)\n",
    "\n",
    "        if self._should_skip_silent_segment(logspec):\n",
    "            return None\n",
    "\n",
    "        return logspec.reshape(self.bands, self.frames, 1)\n",
    "\n",
    "    def extract_features(self, file_name, stretch=True):\n",
    "        wave, sample_rate = self.extract_wave(file_name)\n",
    "        features = []\n",
    "\n",
    "        orig_features_count = 0\n",
    "        for sound_segment in self.sound_segments(wave, self.window_size,\n",
    "                                                 stretch=stretch):\n",
    "            X = self._extract_feature(sound_segment)\n",
    "            if X is not None:\n",
    "                features.append(X)\n",
    "                orig_features_count += 1\n",
    "\n",
    "        aug_features_count = 0\n",
    "        for sound_segment in self.aug_sound_segments(wave, self.window_size,\n",
    "                                                     stretch=stretch):\n",
    "            X = self._extract_feature(sound_segment)\n",
    "            if X is not None:\n",
    "                features.append(X)\n",
    "                aug_features_count += 1\n",
    "\n",
    "        if not features:\n",
    "            if self.verbose:\n",
    "                print('Extracted no sound segments from %s' % file_name)\n",
    "            return None\n",
    "        return np.stack(features), sample_rate, orig_features_count"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Общая логика для обработки наборов данных, в которой также производится сериализация обработанных результатов:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "from collections import defaultdict\n",
    "import pickle\n",
    "\n",
    "import numpy as np\n",
    "\n",
    "class Metadata:\n",
    "    def __init__(self):\n",
    "        self.classes = []\n",
    "\n",
    "    @property\n",
    "    def classes_count(self):\n",
    "        return len(self.classes)\n",
    "\n",
    "    def get_class(self, file_name):\n",
    "        pass\n",
    "\n",
    "\n",
    "class Dataset:\n",
    "    def __init__(self, name, metadata, extraction_type, verbose=1):\n",
    "        self.X_dict = {}\n",
    "        self.y_dict = {}\n",
    "        self.name = name\n",
    "        self.metadata = metadata\n",
    "        self.fold_list = []\n",
    "        self.folds_count = 0\n",
    "        self.fold_feat_idx = defaultdict(list)\n",
    "        self.verbose = verbose\n",
    "\n",
    "        self.extraction_type = extraction_type\n",
    "        if extraction_type == 'logmelspec_30ms_16khz':\n",
    "            self.feature_extractor = LogMelSpecExtractor(self.metadata, fft_window=480, hop_length=240,\n",
    "                                                         sample_rate=16000)\n",
    "        if extraction_type == 'logmelspec_delta_30ms_16khz':\n",
    "            self.feature_extractor = LogMelSpecDeltaExtractor(self.metadata, fft_window=480, hop_length=240,\n",
    "                                                              sample_rate=16000)\n",
    "        else:\n",
    "            raise NotImplementedError('unknown extraction type: %s'%extraction_type)\n",
    "\n",
    "    def get_features_path(self):\n",
    "        return 'features'+os.sep+self.name+os.sep+self.extraction_type\n",
    "\n",
    "    def get_save_file_names(self, fold_name):\n",
    "        features_path = self.get_features_path()\n",
    "        X_file_name = features_path+os.sep+'_'.join((fold_name, 'X.npy'))\n",
    "        y_file_name = features_path+os.sep+'_'.join((fold_name, 'y.npy'))\n",
    "        return X_file_name, y_file_name\n",
    "\n",
    "    def load(self):\n",
    "        features_path = self.get_features_path()\n",
    "        fold_idx_path = features_path+os.sep+'fold_idx.pickle'\n",
    "        if not os.path.exists(fold_idx_path):\n",
    "            return False\n",
    "\n",
    "        with open(fold_idx_path, 'rb') as f:\n",
    "            self.fold_feat_idx = pickle.load(f)\n",
    "            if self.verbose:\n",
    "                print('Loaded fold index')\n",
    "            return True\n",
    "\n",
    "    def save(self, overwrite=False):\n",
    "        print('Saving dataset features')\n",
    "        features_path = self.get_features_path()\n",
    "        if not os.path.exists(features_path):\n",
    "            print('Creating features directory')\n",
    "            os.makedirs(features_path)\n",
    "\n",
    "        for fold_name in sorted(self.X_dict):\n",
    "            X_file_name, y_file_name = self.get_save_file_names(fold_name)\n",
    "            if not overwrite and os.path.exists(X_file_name) and os.path.exists(y_file_name):\n",
    "                print('Features for {0} already exist'.format(fold_name))\n",
    "                continue\n",
    "            X = self.X_dict[fold_name]\n",
    "            y = self.y_dict[fold_name]\n",
    "            print('Saving features for {0} to file'.format(fold_name))\n",
    "            np.save(X_file_name, X)\n",
    "            np.save(y_file_name, y)\n",
    "\n",
    "        fold_idx_path = features_path+os.sep+'fold_idx.pickle'\n",
    "        if overwrite or not os.path.exists(fold_idx_path):\n",
    "            with open(fold_idx_path, 'wb') as f:\n",
    "                pickle.dump(self.fold_feat_idx, f)\n",
    "                if self.verbose:\n",
    "                    print('Saved fold index metadata')\n",
    "\n",
    "    def train_test_split(self, train_folds, test_folds):\n",
    "        if train_folds is None:\n",
    "            train_folds = self.fold_list[:-1]\n",
    "        if test_folds is None:\n",
    "            test_folds = [self.fold_list[-1]]\n",
    "\n",
    "        Xs = []\n",
    "        Ys = []\n",
    "        for fold in train_folds:\n",
    "            X_fold = self.X_dict[fold]\n",
    "            y_fold = self.y_dict[fold]\n",
    "            Xs.append(X_fold)\n",
    "            Ys.append(y_fold)\n",
    "\n",
    "        X_train = np.concatenate(Xs)\n",
    "        y_train = np.hstack(Ys)\n",
    "\n",
    "        Xts = []\n",
    "        Yts = []\n",
    "        for fold in test_folds:\n",
    "            X_fold = self.X_dict[fold]\n",
    "            y_fold = self.y_dict[fold]\n",
    "            Xts.append(X_fold)\n",
    "            Yts.append(y_fold)\n",
    "\n",
    "        X_test = np.concatenate(Xts)\n",
    "        y_test = np.hstack(Yts)\n",
    "\n",
    "        return X_train, X_test, y_train, y_test\n",
    "\n",
    "    def kfold(self, limit_folds=None):\n",
    "        if limit_folds is None:\n",
    "            limit_folds = self.folds_count\n",
    "\n",
    "        for i in range(min(limit_folds, self.folds_count)):\n",
    "            train_folds = self.fold_list[:i]+self.fold_list[i+1:]\n",
    "            test_folds = [self.fold_list[i]]\n",
    "            X_train, X_test, y_train, y_test = self.train_test_split(train_folds, test_folds)\n",
    "            yield X_train, X_test, y_train, y_test, train_folds, test_folds\n",
    "\n",
    "    def kfold_names(self, use_heldout=False):\n",
    "        if use_heldout:\n",
    "            max_fold = self.folds_count-1\n",
    "        else:\n",
    "            max_fold = self.folds_count\n",
    "\n",
    "        for i in range(max_fold):\n",
    "            train_folds = self.fold_list[:i]+self.fold_list[i+1:max_fold]\n",
    "            test_folds = [self.fold_list[i]]\n",
    "            yield train_folds, test_folds"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "SOUND_DIR = 'ESC-50'\n",
    "\n",
    "class ESC50Metadata(Metadata):\n",
    "    def __init__(self):\n",
    "        super().__init__()\n",
    "        self.classes = ['101 - Dog', '102 - Rooster', '103 - Pig', '104 - Cow', '105 - Frog',\n",
    "                        '106 - Cat', '107 - Hen', '108 - Insects (flying)', '109 - Sheep',\n",
    "                        '110 - Crow', '201 - Rain', '202 - Sea waves', '203 - Crackling fire',\n",
    "                        '204 - Crickets', '205 - Chirping birds', '206 - Water drops',\n",
    "                        '207 - Wind', '208 - Pouring water', '209 - Toilet flush',\n",
    "                        '210 - Thunderstorm', '301 - Crying baby', '302 - Sneezing', '303 - Clapping',\n",
    "                        '304 - Breathing', '305 - Coughing', '306 - Footsteps', '307 - Laughing',\n",
    "                        '308 - Brushing teeth', '309 - Snoring', '310 - Drinking - sipping',\n",
    "                        '401 - Door knock', '402 - Mouse click', '403 - Keyboard typing',\n",
    "                        '404 - Door - wood creaks', '405 - Can opening', '406 - Washing machine',\n",
    "                        '407 - Vacuum cleaner', '408 - Clock alarm', '409 - Clock tick',\n",
    "                        '410 - Glass breaking', '501 - Helicopter', '502 - Chainsaw', '503 - Siren',\n",
    "                        '504 - Car horn', '505 - Engine', '506 - Train', '507 - Church bells',\n",
    "                        '508 - Airplane', '509 - Fireworks', '510 - Hand saw']\n",
    "        self.classes_dict = {name: num for num, name in enumerate(self.classes)}\n",
    "\n",
    "    def get_class(self, file_name):\n",
    "        raw_class = file_name.split(os.sep)[-2]\n",
    "        return self.classes_dict[raw_class]\n",
    "\n",
    "class ESC50Dataset(Dataset):\n",
    "    def __init__(self, extraction_type):\n",
    "        super().__init__('esc50', ESC50Metadata(), extraction_type)\n",
    "        self.file_ext = '*.ogg'\n",
    "        self.fold_list = ['fold1', 'fold2', 'fold3', 'fold4', 'fold5']\n",
    "        self.folds_count = 5\n",
    "\n",
    "    def _get_file_paths(self, sound_dir):\n",
    "        for class_name in self.metadata.classes:\n",
    "            pattern = os.path.join(sound_dir, class_name, self.file_ext)\n",
    "            for file_name in glob.iglob(pattern):\n",
    "                yield file_name\n",
    "\n",
    "    def _get_file_fold_num(self, file_path):\n",
    "        return int(file_path.split(os.sep)[-1].split('-')[0])\n",
    "\n",
    "    def load(self, sound_dir=SOUND_DIR):\n",
    "        super().load()\n",
    "        folds_loaded = 0\n",
    "\n",
    "        for fold in self.fold_list:\n",
    "            X_file_name, y_file_name = self.get_save_file_names(fold)\n",
    "\n",
    "            if os.path.exists(X_file_name) and os.path.exists(y_file_name):\n",
    "                print('Loading features from files:', X_file_name, y_file_name)\n",
    "                X_fold = np.load(X_file_name)\n",
    "                y_fold = np.load(y_file_name)\n",
    "                print('Loaded features. Shapes:', X_fold.shape, y_fold.shape)\n",
    "                folds_loaded += 1\n",
    "\n",
    "                self.X_dict[fold] = X_fold\n",
    "                self.y_dict[fold] = y_fold\n",
    "\n",
    "        if folds_loaded == self.folds_count:\n",
    "            return\n",
    "\n",
    "        fold_X = defaultdict(list)\n",
    "        fold_y = defaultdict(list)\n",
    "\n",
    "        fold_last_idx = defaultdict(int)\n",
    "\n",
    "        for file_name in self._get_file_paths(sound_dir):\n",
    "            file_fold = self._get_file_fold_num(file_name)\n",
    "            fold_name = 'fold%d' % file_fold\n",
    "            features = self.feature_extractor.extract_features(file_name)\n",
    "            if features is not None:\n",
    "                X, _, org_features_count = features\n",
    "                y = self.metadata.get_class(file_name)\n",
    "                features_count = X.shape[0]\n",
    "                print('Features count: %d, org features count: %d, class: %d' %\n",
    "                      (features_count, org_features_count, y))\n",
    "\n",
    "                cur_feature_idx = fold_last_idx[fold_name]\n",
    "                self.fold_feat_idx[fold_name].append((cur_feature_idx, cur_feature_idx+org_features_count))\n",
    "                fold_last_idx[fold_name] += features_count\n",
    "\n",
    "                fold_X[fold_name].append(X)\n",
    "                fold_y[fold_name].extend(features_count*[y])\n",
    "\n",
    "        for fold in self.fold_list:\n",
    "            X_fold = np.concatenate(fold_X[fold])\n",
    "            y_fold = np.array(fold_y[fold], dtype=np.int)\n",
    "\n",
    "            self.X_dict[fold] = X_fold\n",
    "            self.y_dict[fold] = y_fold"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "dataset = ESC50Dataset(extraction_type='logmelspec_delta_30ms_16khz')\n",
    "dataset.load()\n",
    "dataset.save()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 8 и 10. Кросс-валидация и настройка гиперпараметров модели. Построение кривых валидации и обучения"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class TrainHistory:\n",
    "    def __init__(self, history):\n",
    "        self.history = history\n",
    "\n",
    "\n",
    "class TestResults:\n",
    "    def __init__(self, y_true, y_pred,\n",
    "                 y_true_oh=None, y_pred_proba=None, classes=None):\n",
    "        self.y_true = y_true\n",
    "        self.y_pred = y_pred\n",
    "        self.y_true_oh = y_true_oh\n",
    "        self.y_pred_proba = y_pred_proba\n",
    "        self.classes = classes\n",
    "        self.frag_acc = -1\n",
    "\n",
    "    def get_f1_score(self, average='weighted'):\n",
    "        return f1_score(self.y_true, self.y_pred, average=average)\n",
    "\n",
    "    def get_accuracy(self):\n",
    "        return accuracy_score(self.y_true, self.y_pred)\n",
    "\n",
    "    def print_classification_report(self):\n",
    "        print(classification_report(self.y_true, self.y_pred,\n",
    "                                    target_names=self.classes, digits=4))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_history(history):\n",
    "    ax1 = plt.subplot(2, 1, 1)\n",
    "    plt.plot(history['acc'])\n",
    "    plt.plot(history['val_acc'])\n",
    "    plt.title('Доля правильных ответов модели')\n",
    "    plt.ylabel('доля прав. ответов')\n",
    "    plt.legend(['обучающая', 'контрольная'], loc='best')\n",
    "    plt.setp(ax1.get_xticklabels(), visible=False)\n",
    "\n",
    "    plt.subplot(2, 1, 2, sharex=ax1)\n",
    "    plt.plot(history['loss'])\n",
    "    plt.plot(history['val_loss'])\n",
    "    plt.title('Функция потерь модели')\n",
    "    plt.ylabel('значение функции')\n",
    "    plt.xlabel('эпохи обучения')\n",
    "\n",
    "    plt.tight_layout()\n",
    "    plt.show()\n",
    "\n",
    "def evaluate(model, dataset, use_heldout=False, epochs=30, min_lr=0.0002):\n",
    "    accuracies = []\n",
    "    frag_accuracies = []\n",
    "    f1s = []\n",
    "    for train_folds, test_folds in dataset.kfold_names(use_heldout=use_heldout):\n",
    "        results = model.train_on(dataset, train_folds, test_folds,\n",
    "                                 epochs=epochs, validate=True, min_lr=min_lr)\n",
    "        print('Model evaluation results:')\n",
    "        print('Trained on %s, validated on %s' % (', '.join(train_folds), ', '.join(test_folds)))\n",
    "        print('Fragment accuracy: %.4f' % results.frag_acc)\n",
    "        frag_accuracies.append(results.frag_acc)\n",
    "        accuracy = results.get_accuracy()\n",
    "        accuracies.append(accuracy)\n",
    "        print('Accuracy: %.4f' % accuracy)\n",
    "        f1_score = results.get_f1_score(average='weighted')\n",
    "        f1s.append(f1_score)\n",
    "        print('F1-Score (weighted): %.4f' % f1_score)\n",
    "        results.print_classification_report()\n",
    "\n",
    "        if model.history is not None:\n",
    "            plot_history(model.history)\n",
    "        else:\n",
    "            print('Learning history is not found')\n",
    "\n",
    "    print('Final statistics:')\n",
    "    print('Fragment accuracy. Mean: %.4f, std: %.4f' % (np.mean(frag_accuracies),\n",
    "                                                        np.std(frag_accuracies)))\n",
    "    print('Accuracy. Mean: %.4f, std: %.4f' % (np.mean(accuracies), np.std(accuracies)))\n",
    "    print('F1-Score. Mean: %.4f, std: %.4f' % (np.mean(f1s), np.std(f1s)))\n",
    "\n",
    "model = AlexLikeModel(lr=0.0003, suff='_lr0-0003')\n",
    "evaluate(model, dataset, use_heldout=True, epochs=50)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Подбор архитектуры сети (слоёв и их параметров), а также параметров предобработки был тактично опущен, т.к. он осуществлялся мной в том числе в рамках моей выпускной работы. Чтобы повторить все вычисления заново, потребовалось бы несколько суток непрерывной работы компьютера со 100% загрузкой видеокарты, что я сейчас не могу сделать. Прошу не снижать за это баллы :(\n",
    "\n",
    "Могу отметить, что при увеличении Dropout модель недообучается, а при увеличении learning rate модель сильно колбасит и её итоговое качество непредсказуемо, но обычно ниже той, что достигается при более низких значениях.\n",
    "\n",
    "По кривым обучения можно видеть, что модель не переобучается, а выходит на плато. Если добавить больше данных и усложить модель, можно достигнуть более высоких результатов."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 9. Создание новых признаков и описание этого процесса"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Помимо логарифмированных мел-спектрограмм можно использовать дельта-коэффициенты, что и было сделано в экспериментах выше. О том, что это такое, можно прочитать <a href=\"http://practicalcryptography.com/miscellaneous/machine-learning/guide-mel-frequency-cepstral-coefficients-mfccs/\">здесь</a> и <a href=\"https://librosa.github.io/librosa/generated/librosa.feature.delta.html\">здесь</a>. Если в кратце, то они представляют собой аналог численной производной.\n",
    "\n",
    "Посчитанные коэффициенты добавляются к исходным признакам в виде \"канала\" по аналогии с каналами в изображении. После добавления мы получаем уже четырёхмерную матрицу данных. Также в качестве каналов можно добавить дельта-коэффициенты от дельта-коэффициентов, а также MFCC.\n",
    "Эксперименты за кулисами показали увеличение качества лишь от добавления дельта-коэффициентов на наборе ESC-50."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 11. Прогноз для тестовой или отложенной выборки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def evaluate_validation(model, dataset, epochs=30, min_lr=0.0002):\n",
    "    accuracies = []\n",
    "    frag_accuracies = []\n",
    "    f1s = []\n",
    "    train_folds = ['fold1', 'fold2', 'fold3', 'fold4']\n",
    "    test_folds = ['fold5']\n",
    "    results = model.train_on(dataset, train_folds, test_folds,\n",
    "                             epochs=epochs, validate=True, min_lr=min_lr)\n",
    "    print('Model evaluation results:')\n",
    "    print('Trained on %s, validated on %s' % (', '.join(train_folds), ', '.join(test_folds)))\n",
    "    print('Fragment accuracy: %.4f' % results.frag_acc)\n",
    "    frag_accuracies.append(results.frag_acc)\n",
    "    accuracy = results.get_accuracy()\n",
    "    accuracies.append(accuracy)\n",
    "    print('Accuracy: %.4f' % accuracy)\n",
    "    f1_score = results.get_f1_score(average='weighted')\n",
    "    f1s.append(f1_score)\n",
    "    print('F1-Score (weighted): %.4f' % f1_score)\n",
    "    results.print_classification_report()\n",
    "\n",
    "    if model.history is not None:\n",
    "        plot_history(model.history)\n",
    "    else:\n",
    "        print('Learning history is not found')\n",
    "\n",
    "model = AlexLikeModel(lr=0.0003, suff='_lr0-0003')\n",
    "evaluate_validation(model, dataset, epochs=50)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Полученные результаты согласуются с данными кросс-валидации (отличие около 1%).\n",
    "\n",
    "При помощи Tensorflow невозможно корректно зафиксировать зерно генератора псведослучайных чисел, т.к. библиотека cuDNN, которая задействуется внутри, в некоторых местах использует свой ГПСЧ, на который пока нельзя повлиять. Поэтому ваши результаты могут отличаться от моих, но незначительно."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 12. Выводы"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Данная модель хорошо показала себя при решении задачи классификации звуковых сигналов. Приведу сравнение с результатами автора набора данных (средняя доля правильных ответов на кросс-валидации):\n",
    "* Мой результат - 73.4%\n",
    "* k-NN - 32.2%\n",
    "* Random Forest - 45.1%\n",
    "* SVM - 39.5%"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Я не стал подробно расписывать каждый параметр модели и то, почему именно такой был выбран, т.к. для этого потребовалось бы притащить теории на десятки страниц, потому что в курсе не разбирались нейросетевые модели. Если есть вопросы, могу поделиться своим небольшим опытом в чате ODS, а также выслушать вашу критику и пожелания: @fremis"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Сейчас данная тема активно исследуется. На arXiv вы можете увидеть новые статьи по данной тематике чуть ли не каждый день. Даже проводится ежегодный конкурс DCASE, который проводится при спонсировании Google."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "В этом году появился огромный набор данных от Google под названием Audioset, в котором содержится огромное количество звуков из видео на Youtube, разбитых на огромное количество категорий. Для того, чтобы переварить этот набор, потребуются уже более глубокие сети и соответствующие мощности. Также имеется огромное множество неразмеченных звуковых файлов, которые было бы неплохо задействовать при обучении модели (например, есть набор ESC-US от автора ESC-50, в котором содержится довольно много неразмеченных звуковых файлов). Для этого можно попытаться применить подходы, которые используются при работе с изображениями."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.5.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
